package org.visionlibrary.image.geomtric.lines.search;

import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.media.jai.TiledImage;

import org.visionlibrary.image.geomtric.lines.Lines;
import org.visionlibrary.image.geomtric.lines.search.model.SegmentAccepterStrategy;
import org.visionlibrary.image.geomtric.lines.search.model.SegmentFinder;
import org.visionlibrary.image.geomtric.lines.search.model.SegmentTrackingStrategy;
import org.visionlibrary.image.geomtric.model.Point2d;
import org.visionlibrary.image.geomtric.model.Vector2d;
import org.visionlibrary.image.geomtric.util.LineExtractorUtil;
import org.visionlibrary.image.geomtric.util.Utils;


/**
 * Klasa realizujaca algorytm wektoryzacji podazania za pikselami. Opis algorytmu
 * znajduje sie w pracy pisemnej.
 */
public class FollowerSegmentFinder implements SegmentFinder {
	public final static int DEFAULT_NEIGHBOURHOOD_SIZE = 3;
	public final static int DEFAULT_DISTANCE_FROM_CONTOUR = 5;
	public final static int DEFAULT_OUTLINE_COLOR = 0xFFFFFF;
	
	private int neighbourhoodSize = 3;
	private int maximalDistanceFromLine = 20;
	private int outlineColor = 0xffffff;

	private ModifiedDefaultLineAccepterStrategy accepter = null;
	private ModifiedDefaultLineTrackingStrategy tracker = null;

	private int step = 1;
	private int removeSize = 1;

	public FollowerSegmentFinder() {
		this(DEFAULT_NEIGHBOURHOOD_SIZE, DEFAULT_DISTANCE_FROM_CONTOUR, DEFAULT_OUTLINE_COLOR);
	}

	public FollowerSegmentFinder(int neighbourSize, int minimalLineLength) {
		this(neighbourSize, minimalLineLength, DEFAULT_OUTLINE_COLOR);
	}

	public FollowerSegmentFinder(int neighbourhoodSize, int maximalDistanceFromLine, int outlineColor) {
		this.neighbourhoodSize = neighbourhoodSize;
		this.maximalDistanceFromLine = maximalDistanceFromLine;
		this.outlineColor = outlineColor;
	}

	@Override
	public List<Vector2d> search(TiledImage image) {
		if (null == image)
			throw new IllegalArgumentException("Image is null.");
		
		accepter = new ModifiedDefaultLineAccepterStrategy(maximalDistanceFromLine);
		tracker = new ModifiedDefaultLineTrackingStrategy(outlineColor, neighbourhoodSize);
		tracker.setImage(image.getAsBufferedImage());

		List<List<Point2d>> outlines = extractContour(image.getAsBufferedImage());
		List<Vector2d> result = new ArrayList<Vector2d>();

		for (List<Point2d> outline : outlines) {
			int start = 0;
			while (true) {
				SegmentWithPoints line = extractLine(outline, start);
				if (null == line) {
					break;
				}
				Point2d pointB = line.segment.getLast();
				removeNearestPoint(outline, line, pointB);
				
				result.add(line.segment);
				start = outline.indexOf(pointB);
			}
		}

		return result;
	}

	private void removeNearestPoint(List<Point2d> outline, SegmentWithPoints line, Point2d pointB) {
		for (Point2d point : line.points)
			for (int i = -this.removeSize; i <= this.removeSize; i++)
				for (int j = -this.removeSize; j <= this.removeSize; j++) {
					Point2d p = new Point2d(point.x + i, point.y + j);
					if (!p.equals(pointB))
						outline.remove(p);
				}
	}

	private List<List<Point2d>> extractContour(BufferedImage image) {
		List<List<Point2d>> lines = new ArrayList<List<Point2d>>();
		
		int outlineColor = tracker.getOutlineColor();
		for (int y = 0; y < image.getHeight(); y++) {
			for (int x = 0; x < image.getWidth(); x++) {
				boolean o = (image.getRGB(x, y) & 0xFFFFFF) == outlineColor;
				if (o) {
					traceLine(lines, x, y);
				}
			}
		}

		return lines;
	}

	private SegmentWithPoints extractLine(List<Point2d> outline, int start) {
		if (start == outline.size() - 1) {
			return null;
		}
		
		Point2d a = outline.get(start);
		Point2d b = outline.get(outline.size() - 1);
		
		if(a.equals(b)) {
			return null;
		}
		
		Vector2d l = null;
		boolean last = false;
		int end = start + this.step <= outline.size() ? start + this.step : outline.size() - 1;
		
		while (end < outline.size()) {
			
			b = outline.get(end);
			l = new Vector2d(a, b);

			if (accepter.accept(outline, start, end, l)) {
				break;
			}
			if (end == outline.size() - 1)
				break;
			
			if ((end + this.step >= outline.size()) && (!last)) {
				last = true;
				end = outline.size() - 1;
			} else {
				end += this.step;
			}

		}

		return new SegmentWithPoints(l, new ArrayList<Point2d>(outline.subList(start, end)));
	}

	private void traceLine(List<List<Point2d>> lines, int x, int y) {
		List<Point2d> line = new ArrayList<Point2d>();

		Point2d point = new Point2d(x, y);
		line.add(point);
		while (true) {
			point = tracker.nextPoint(point);
			
			if (point != null) {
				line.add(point);
			} else {
				break;
			}
		}
		if(0 != line.size() - 1) {
			lines.add(line);
		}
	}

	private class SegmentWithPoints {
		private Vector2d segment;
		private List<Point2d> points;

		private SegmentWithPoints(Vector2d segment, List<Point2d> points) {
			this.segment = segment;
			this.points = points;
		}
	}
}

class ModifiedDefaultLineAccepterStrategy implements SegmentAccepterStrategy {
	private final int minLength;

	public ModifiedDefaultLineAccepterStrategy() {
		this(20);
	}

	public ModifiedDefaultLineAccepterStrategy(int minLength) {
		this.minLength = minLength;
	}

	public boolean accept(List<Point2d> pointLine, int start, int end, Vector2d line) {
		double max = 4.9E-324D; //Double.MAX_VALUE
		double min = 1.7976931348623157E+308D; //Double.MIN_VALUE
		for (int j = start + 1; j < end - 1; j++) {
			double d = Lines.lineSegmentToPointDistance(line, pointLine.get(j));
			d = Math.abs(d);
			if (max < d) {
				max = d;
			}
			if (min > d) {
				min = d;
			}
		}
		return max > this.minLength;
	}
}

class ModifiedDefaultLineTrackingStrategy implements SegmentTrackingStrategy {
	private ThreadLocal<BufferedImage> threadsafeimage = new ThreadLocal<BufferedImage>();
	private final int contourColor;
	private final int neighbourhoodSize;
	private int width;
	private int height;
	private final Map<Integer, List<Point2d>> neighboursPoint = new HashMap<Integer, List<Point2d>>();

	public ModifiedDefaultLineTrackingStrategy(int lineColor, int neighborSize) {
		this.contourColor = lineColor;
		this.neighbourhoodSize = neighborSize;
		setupNeighborsPoint(neighborSize);
	}

	private void setupNeighborsPoint(int neighborSize) {
		for (int i = 1; i <= neighborSize; i++) {
			List<Point2d> list = getListPoint(i);

			this.neighboursPoint.put(Integer.valueOf(i), list);
		}
	}

	private static List<Point2d> getListPoint(int distance) {
		List<Point2d> list = new ArrayList<Point2d>();

		int y = -distance;
		for (int xx = -distance; xx < distance; xx++) {
			list.add(new Point2d(xx, y));
		}

		int x = distance;
		for (int yy = -distance; yy < distance; yy++) {
			list.add(new Point2d(x, yy));
		}
		y = distance;
		for (int xx = distance; xx > -distance; xx--) {
			list.add(new Point2d(xx, y));
		}
		x = -distance;
		for (int yy = distance; yy > -distance; yy--) {
			list.add(new Point2d(x, yy));
		}
		return list;
	}

	private Point2d convertDirection(Point2d dir, int size) {
		if (dir == null) {
			return null;
		}

		double x = dir.x;
		double y = dir.y;
		double max = Math.max(Math.abs(x), Math.abs(y));

		x = x / max * size;
		y = y / max * size;

		return new Point2d(Math.round(x), Math.round(y));
	}

	public ModifiedDefaultLineTrackingStrategy() {
		this(16777215, 5);
	}

	public Point2d nextPoint(Point2d actualPoint) {
		BufferedImage img = (BufferedImage) this.threadsafeimage.get();
		int color = img.getRGB((int) (actualPoint.x), (int) (actualPoint.y)) & 0xFFFFFF;
		Point2d dirPoint = null;
		if (color == this.contourColor) {
			color = Utils.generateColorValue();
			img.setRGB((int) (actualPoint.x), (int) (actualPoint.y), color);
		} else {
			Point2d prev = findNeighborPoint(img, actualPoint, null, -1, color, this.width, this.height);
			
			if(null == prev)
				return null;
			
			dirPoint = new Point2d(actualPoint.x - prev.x, actualPoint.y - prev.y);
		}
		return findNeighborPoint(img, actualPoint, dirPoint, color,
				this.contourColor, this.width, this.height);
	}

	private Point2d findNeighborPoint(BufferedImage img, Point2d actualPoint,
			Point2d dirPoint, int color, int lineColor, int w, int h) {
		for (int i = 1; i <= this.neighbourhoodSize; i++) {
			Point2d point = findNearestPoint(img, actualPoint, dirPoint, i,
					lineColor);
			if (point != null) {
				if (color > -1) {
					img.setRGB((int) point.x, (int) point.y, color);
				}
				return point;
			}
		}
		return null;
	}

	private Point2d findNearestPoint(BufferedImage img, Point2d point, Point2d dirPoint, int s, int findColor) {
		Point2d direction = convertDirection(dirPoint, s);
		if (direction == null) {
			for (Point2d p : (List<Point2d>) this.neighboursPoint.get(Integer.valueOf(s))) {
				int xx = (int) (point.x + p.x);
				int yy = (int) (point.y + p.y);
				if (isOutOfBounds(xx, yy)) {
					continue;
				}
				boolean o = (img.getRGB(xx, yy) & 0xFFFFFF) == findColor;
				if (o)
					return new Point2d(xx, yy);
			}
		} else {
			List<Point2d> pointsList = (List<Point2d>) this.neighboursPoint.get(Integer.valueOf(s));
			int xx = (int) (point.x + dirPoint.x);
			int yy = (int) (point.y + dirPoint.y);
			if (isOutOfBounds(xx, yy)) {
				return null;
			}
			boolean o = (img.getRGB(xx, yy) & 0xFFFFFF) == findColor;
			if (o) {
				return new Point2d(xx, yy);
			}
			int start = pointsList.indexOf(direction);
			int size = pointsList.size();
			int p = (size - 2) / 2;
			for (int i = 1; i <= p; i++) {
				for (int z = -1; z <= 1; z += 2) {
					Point2d dir = (Point2d) pointsList.get((start + z * i + size) % size);
					xx = (int) (point.x + dir.x);
					yy = (int) (point.y + dir.y);
					if (isOutOfBounds(xx, yy)) {
						continue;
					}
					o = (img.getRGB(xx, yy) & 0xFFFFFF) == findColor;
					if (o) {
						return new Point2d(xx, yy);
					}
				}
			}
		}

		return null;
	}

	private boolean isOutOfBounds(int xx, int yy) {
		return (xx < 0) || (xx >= this.width) || (yy < 0) || (yy >= this.height);
	}

	public BufferedImage getImage() {
		return (BufferedImage) this.threadsafeimage.get();
	}

	public void setImage(BufferedImage image) {
		this.threadsafeimage.set(LineExtractorUtil.copyBufferedImage(image));
		this.width = image.getWidth();
		this.height = image.getHeight();
	}

	public int getOutlineColor() {
		return this.contourColor;
	}
}